package han.chensing.latos.game.processor;

import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.Color;

import java.util.List;

import han.chensing.latos.game.elements.LatosElement;
import han.chensing.latos.game.elements.effect.BadEffect;
import han.chensing.latos.game.elements.effect.HitEffect;
import han.chensing.latos.game.elements.notes.DirectFlickNote;
import han.chensing.latos.game.elements.notes.DragNote;
import han.chensing.latos.game.elements.notes.FlickNote;
import han.chensing.latos.game.elements.notes.HoldNote;
import han.chensing.latos.game.elements.notes.Line;
import han.chensing.latos.game.elements.notes.Note;
import han.chensing.latos.game.elements.notes.TapNote;
import han.chensing.latos.game.elements.stage.LatosStage;
import han.chensing.latos.game.input.TouchContainer;
import han.chensing.latos.game.util.MathUtil;
import han.chensing.latos.res.Res;

public class JudgeHandler {
    public static final float PERFECT=.085f;
    public static final float GOOD=.165f;
    public static final float BAD=.180f;
    public static final float MISS=.200f;

    public static final int judgeRange=10;
    public static final int judgeRangeBack=5;
    public static final int holdRangeBack=10;
    public static final int directFlickDepth =0;

    public static final float judgeWidth=210f;
    public static final float flickDistance=1000f;
    public static final float directFlickDistance=250f;
    public static final float holdPardonEndLength=350f;

    public boolean auto=false;

    int flickDepthNow;
    float flickLastTime;
    boolean hasFlickThisTime;

    public void judgeLines(
            float nowTime,
            LatosStage lineStage,
            LatosStage effectStage,
            TouchContainer touchContainer,
            ComboCounter comboCounter
    ){
        Color currentLineColor=comboCounter.isAP()?
                Line.yellowLineColor:(comboCounter.isFC()?Line.blueLineColor:Line.whiteLineColor);
        List<LatosElement> elements = lineStage.getElements();
        hasFlickThisTime=false;
        for (LatosElement element:elements){
            Line line=(Line) element;
            judgeNormalNotes(nowTime,line,effectStage,touchContainer,comboCounter);
            judgeHoldNotes(nowTime,line,effectStage,touchContainer,comboCounter);
            line.setLineColor(currentLineColor);
        }
    }

    private void judgeHoldNotes(
            float nowTime,
            Line line,
            LatosStage effectStage,
            TouchContainer touchContainer,
            ComboCounter comboCounter){
        List<HoldNote> holdNotes = line.getHoldNotes();
        int holdJudgeIndex=line.getHoldJudgeIndex();
        int i=Math.max(holdJudgeIndex-judgeRangeBack,0);
        int j=holdJudgeIndex;
        int max = Math.max(0, Math.min(
                holdJudgeIndex + judgeRange,
                holdNotes.size()));
        for(; i< max; i++,j++) {
            HoldNote holdNote = holdNotes.get(i);
            if (holdNote.getHitStatue()!= Note.HitStatue.None)continue;
            float[] foot = MathUtil.getPosPointVerticalInLine(
                    line.getX(), line.getY(), line.getRotation(),
                    holdNote.getTrueX(), holdNote.getTrueY());
            JudgeResult judgeResult = judgeHoldNote(holdNote, foot, touchContainer, nowTime);
            if (judgeResult.hit){
                genEffect(effectStage,foot,
                        holdNote.getTempHitStatue()== Note.HitStatue.Perfect?
                                HitEffect.Type.Perfect: HitEffect.Type.Good
                );
            }
            if (judgeResult.hitStatue!= Note.HitStatue.None&&!holdNote.isScoreCounted()){
                comboCounter.updateScore(holdNote);
                holdNote.setScoreCounted(true);
                line.setHoldDrawIndex(Math.max(line.getHoldDrawIndex(),i));
                line.setHoldJudgeIndex(Math.max(line.getHoldJudgeIndex(),i));
            }
        }
    }

    private void judgeNormalNotes(
            float nowTime,
            Line line,
            LatosStage effectStage,
            TouchContainer touchContainer,
            ComboCounter comboCounter){
        boolean canUpdateIndex=true;
        List<Note> notes = line.getNormalNotes();
        int judgeIndex = line.getNormalJudgeIndex();
        int i=Math.max(judgeIndex,0);
        int j=judgeIndex;
        flickDepthNow= directFlickDepth +1;
        flickLastTime=0;
        int max = Math.max(0, Math.min(
                judgeIndex + judgeRange, notes.size()));
        for(; i< max; i++,j++){
            Note note = notes.get(i);
            JudgeResult judgeResult = judgeNormalNote(note,effectStage, nowTime, touchContainer);
            if (note.getHitStatue()==Note.HitStatue.None
                    &&canUpdateIndex) {
                canUpdateIndex=false;
            }
            if (judgeResult.hitStatue!= Note.HitStatue.None&&!note.isScoreCounted()){
                comboCounter.updateScore(note);
                note.setScoreCounted(true);
            }
            if (!canUpdateIndex)
                continue;
            if (judgeResult.hit&&judgeResult.updateJudgeIndex){
                if (judgeResult.delete){
                    line.setNormalJudgeIndex(j+1);
                    line.setNormalDrawIndex(j+1);
                }
            }
            if (judgeResult.missed){
                line.setNormalJudgeIndex(j+1);
                line.setNormalDrawIndex(j+1);
            }
        }
        touchContainer.setNeedsUpdateDistance(hasFlickThisTime);
    }

    private JudgeResult judgeNormalNote(
            Note note,
            LatosStage effectStage,
            float nowTime,
            TouchContainer container){
        if (isNoteMissed(note,nowTime)) {
            note.setShown(false);
            note.setHitStatue(Note.HitStatue.Miss);
            JudgeResult judgeResult = new JudgeResult() {{
                missed = true;
            }};
            judgeResult.hitStatue= Note.HitStatue.Miss;
            return judgeResult;
        }
        if (hasJudgeResult(note)) {
            note.setShown(false);
            return new JudgeResult();
        }
        Line line = note.getBindLine();
        float[] foot = MathUtil.getPosPointVerticalInLine(
                line.getX(), line.getY(), line.getRotation(),
                note.getTrueX(), note.getTrueY());
        Note.NoteType noteType = note.getNoteType();
        switch (noteType){
            case Tap: {
                JudgeResult judgeResult = judgeTapNote((TapNote) note, foot, container, nowTime);
                if (judgeResult.delete && judgeResult.hit) {
                    Note.HitStatue hitStatue = judgeResult.hitStatue;
                    if (hitStatue == Note.HitStatue.Good ||
                            hitStatue == Note.HitStatue.Perfect) {
                        genEffect(effectStage, foot,
                                hitStatue == Note.HitStatue.Perfect ?
                                        HitEffect.Type.Perfect : HitEffect.Type.Good);
                    }
                    if (hitStatue== Note.HitStatue.Bad){
                        genBadEffect(effectStage,note);
                    }
                    note.setShown(false);
                }
                return judgeResult;
            }
            case Drag: {
                JudgeResult judgeResult = judgeDragNote((DragNote) note, foot, container, nowTime);
                if (judgeResult.hit&& judgeResult.delete){
                    genEffect(effectStage, foot, HitEffect.Type.Perfect);
                }
                return judgeResult;
            }
            case Flick:{
                hasFlickThisTime=true;
                JudgeResult judgeResult = judgeFlickNote((FlickNote) note, foot, container, nowTime);
                if (judgeResult.hit&& judgeResult.delete){
                    genEffect(effectStage,foot, HitEffect.Type.Perfect);
                }
                return judgeResult;
            }
            case Hold:{
                break;
            }
            case DirectFlick:{
                DirectFlickNote directFlickNote=(DirectFlickNote) note;
                JudgeResult judgeResult = judgeDirectFlickNote(directFlickNote, foot, container, nowTime);
                if (judgeResult.hit&& judgeResult.delete){
                    genEffect(effectStage,foot, HitEffect.Type.Perfect);
                }
                return judgeResult;
            }
        }
        return new JudgeResult();
    }

    private boolean isNoteMissed(Note note,float nowTime){
        return ((note.getHitStatue() == Note.HitStatue.None &&
                note.getHitTime() + MISS<= nowTime)
                ||note.getHitStatue()== Note.HitStatue.Miss)&&
                note.getNoteType()!= Note.NoteType.Hold;
    }

    private boolean hasJudgeResult(Note note){
        return note.getHitStatue()!= Note.HitStatue.None;
    }

    private JudgeResult judgeDirectFlickNote(
            DirectFlickNote directFlickNote,
            float[] foot,TouchContainer container,float nowTime){
        TouchContainer.Touch[] touches = container.getTouches();
        Line line = directFlickNote.getBindLine();
        float hitTime = directFlickNote.getHitTime();
        if (flickDepthNow<=0)
            return new JudgeResult();
        if (hitTime>flickLastTime){
            flickLastTime=hitTime;
            flickDepthNow--;
        }
        if (directFlickNote.isFlicked()||auto){
            if (nowTime>= hitTime){
                playSound(directFlickNote);
                directFlickNote.setHitStatue(Note.HitStatue.Perfect);
                return new JudgeResult(){{
                    hitStatue = Note.HitStatue.Perfect;
                    hit = true;
                }};
            }
            return new JudgeResult();
        }
        for (TouchContainer.Touch touch : touches) {
            if (!touch.isTouched())
                continue;
            boolean judged = MathUtil.isPointInJudgeRange(
                    foot[0], foot[1], line.getRotation(),
                    touch.getX(), touch.getY(), judgeWidth);
            if (!judged)
                continue;
            float startPointDistanceToLine = MathUtil.getDistanceOfPointToLine(
                    line.getX(), line.getY(), line.getRotation(), touch.getStartX(), touch.getStartY());
            float pointDistanceToLine = MathUtil.getDistanceOfPointToLine(
                    line.getX(), line.getY(), line.getRotation(), touch.getX(), touch.getY());
            directFlickNote.setDirectDistance(
                    (int)
                            (directFlickNote.getDirectDistance() +
                                    (pointDistanceToLine-startPointDistanceToLine)*(directFlickNote.isUp()?1:-1)));
            touch.setStartX(touch.getX());
            touch.setStartY(touch.getY());
            if (directFlickNote.getDirectDistance()<=directFlickDistance)
                continue;
            Note.HitStatue judgeResult = getJudgeResult(directFlickNote, nowTime);
            if (judgeResult== Note.HitStatue.Perfect||
                    judgeResult== Note.HitStatue.Good){
                directFlickNote.setFlicked(true);
                touch.setStartX(touch.getX());
                touch.setStartY(touch.getY());
                touch.setDragDistance(0);
                break;
            }
        }
        return new JudgeResult();
    }

    private JudgeResult judgeHoldNote(
            HoldNote holdNote,
            float[] foot,TouchContainer container,float nowTime) {
        Note.HitStatue hitStatue = getJudgeResult(holdNote, nowTime);
        if (!holdNote.isHolding()&&hitStatue==Note.HitStatue.Bad&&
                hitStatue.getOffset()<=0)
            holdNote.setMissed(true);
        if (holdNote.isMissed()&&hitStatue==Note.HitStatue.None) {
            if (holdNote.getTrueLength() + holdNote.getDistanceHeight()<= 0) {
                holdNote.setShown(false);
                holdNote.setHitStatue(Note.HitStatue.Miss);
                return new JudgeResult() {{
                    hitStatue= Note.HitStatue.Miss;
                    missed = true;
                }};
            }
            return new JudgeResult(){{
                delete=true;
            }};
        }
        TouchContainer.Touch[] touches = container.getTouches();
        Line line = holdNote.getBindLine();
        float lastJudgeTime = holdNote.getLastJudgeTime();
        float diffTime = nowTime - lastJudgeTime;
        float leftTime = holdNote.getJudgePardon() - diffTime;
        holdNote.setLastJudgeTime(nowTime);
        holdNote.setJudgePardon(leftTime);
        for (TouchContainer.Touch touch: touches){
            if (!touch.isTouched()&&!auto)
                continue;
            boolean judged = MathUtil.isPointInJudgeRange(
                    foot[0], foot[1], line.getRotation(),
                    touch.getX(), touch.getY(), judgeWidth);
            if (!judged&&!auto)
                continue;
            if (auto&&nowTime>=holdNote.getHitTime()&&!holdNote.isHolding()) {
                holdNote.setHolding(true);
                holdNote.setTempHitStatue(Note.HitStatue.Perfect);
                playSound(holdNote);
            }else if ((!holdNote.isHolding()&&!touch.isTouchHoldHandled())&&!auto) {
                if (!(hitStatue == Note.HitStatue.Perfect ||
                        hitStatue == Note.HitStatue.Good))
                    continue;
                holdNote.setHolding(true);
                holdNote.setTempHitStatue(hitStatue);
                playSound(holdNote);
            }
            touch.setTouchHoldHandled(true);
            touch.setTouchTapHandled(true);
            if (holdNote.isHolding()&&leftTime<=0){
                holdNote.resetJudgePardon();
                if (holdNote.getTrueLength()+holdNote.getDistanceHeight()-holdPardonEndLength<=0) {
                    Note.HitStatue tempHitStatue = holdNote.getTempHitStatue();
                    holdNote.setHitStatue(tempHitStatue);
                    JudgeResult judgeResult = new JudgeResult() {{
                        hit = true;
                        delete = false;
                    }};
                    judgeResult.hitStatue= tempHitStatue;
                    return judgeResult;
                }
                return new JudgeResult(){{
                    hit=true;
                    delete=true;
                }};
            }
        }
        if (holdNote.isHolding()&&leftTime<=0&&
                holdNote.getHitStatue()==Note.HitStatue.None)
            holdNote.setMissed(true);
        if (holdNote.getHitStatue()!=Note.HitStatue.None&&
                holdNote.getTrueLength()+holdNote.getDistanceHeight()<=0) {
            return new JudgeResult(){{
                delete=true;
            }};
        }
        return new JudgeResult(){{
            delete=false;
        }};
    }

    private JudgeResult judgeFlickNote(
            FlickNote flickNote,
            float[] foot,TouchContainer container,float nowTime){
        if (flickNote.isFlicked()){
            if (nowTime>=flickNote.getHitTime()){
                playSound(flickNote);
                flickNote.setHitStatue(Note.HitStatue.Perfect);
                return new JudgeResult(){{
                    hitStatue = Note.HitStatue.Perfect;
                    hit = true;
                }};
            }
            return new JudgeResult();
        }
        TouchContainer.Touch[] touches = container.getTouches();
        Line line = flickNote.getBindLine();
        for (TouchContainer.Touch touch : touches) {
            if (!touch.isTouched())
                continue;
            boolean judged = MathUtil.isPointInJudgeRange(
                    foot[0], foot[1], line.getRotation(),
                    touch.getX(), touch.getY(), judgeWidth);
            if (!judged)
                continue;
            if (touch.getDragDistance()<=flickDistance)
                continue;
            Note.HitStatue judgeResult = getJudgeResult(flickNote, nowTime);
            if (judgeResult== Note.HitStatue.Perfect||
                    judgeResult== Note.HitStatue.Good){
                flickNote.setFlicked(true);
                touch.setStartX(touch.getX());
                touch.setStartY(touch.getY());
                touch.setDragDistance(0);
                break;
            }
        }
        return new JudgeResult();
    }

    private JudgeResult judgeDragNote(
            DragNote dragNote,
            float[] foot,TouchContainer container,float nowTime){
        if (dragNote.isDragged()||auto) {
            if (nowTime >= dragNote.getHitTime()) {
                dragNote.setHitStatue(Note.HitStatue.Perfect);
                playSound(dragNote);
                return new JudgeResult() {{
                    hitStatue = Note.HitStatue.Perfect;
                    hit = true;
                }};
            }
            return new JudgeResult();
        }
        TouchContainer.Touch[] touches = container.getTouches();
        Line line = dragNote.getBindLine();
        for (TouchContainer.Touch touch:touches) {
            if (!touch.isTouched())
                continue;
            boolean judged = MathUtil.isPointInJudgeRange(
                    foot[0], foot[1], line.getRotation(),
                    touch.getX(), touch.getY(), judgeWidth);
            if (!judged)
                continue;
            Note.HitStatue judgeResult = getJudgeResult(dragNote, nowTime);
            if (judgeResult== Note.HitStatue.Perfect||
                judgeResult== Note.HitStatue.Good){
                touch.setTouchTapHandled(true);
                touch.setTouchHoldHandled(true);
                dragNote.setDragged(true);
            }
            break;
        }
        return new JudgeResult();
    }

    private JudgeResult judgeTapNote(
            TapNote tapNote,
            float[] foot,TouchContainer container,float nowTime){
        if (auto&&nowTime>=tapNote.getHitTime()){
            playSound(tapNote);
            tapNote.setHitStatue(Note.HitStatue.Perfect);
            return new JudgeResult() {{
                hit = true;
                hitStatue= Note.HitStatue.Perfect;
            }};
        }
        TouchContainer.Touch[] touches = container.getTouches();
        Line line = tapNote.getBindLine();
        for (TouchContainer.Touch touch:touches) {
            if (!touch.isTouched())
                continue;
            if (touch.isTouchTapHandled())
                continue;
            boolean judged = MathUtil.isPointInJudgeRange(
                    foot[0], foot[1], line.getRotation(),
                    touch.getX(), touch.getY(), judgeWidth);
            if (!judged)
                continue;
            Note.HitStatue hitStatue = getJudgeResult(tapNote, nowTime);
            boolean none=false;
            switch (hitStatue) {
                case Perfect:
                case Good:
                    playSound(tapNote);
                    break;
                case Bad:
                    break;
                default:
                case None:
                    none=true;
            }
            touch.setTouchTapHandled(true);
            touch.setTouchHoldHandled(true);
            if(none) return new JudgeResult();
            JudgeResult judgeResult1 = new JudgeResult() {{
                hit = true;
            }};
            judgeResult1.hitStatue=hitStatue;
            tapNote.setHitStatue(hitStatue);
            return judgeResult1;
        }
        return new JudgeResult();
    }

    private Note.HitStatue getJudgeResult(Note note, float nowTime){
        float hitTime = note.getHitTime();
        if (nowTime<=hitTime+PERFECT&&nowTime>=hitTime-PERFECT)
            return Note.HitStatue.Perfect.setOffset(hitTime-nowTime);
        if (nowTime<=hitTime+GOOD&&nowTime>=hitTime-GOOD)
            return Note.HitStatue.Good.setOffset(hitTime-nowTime);
        if (nowTime<=hitTime+BAD&&nowTime>=hitTime-BAD)
            return Note.HitStatue.Bad.setOffset( hitTime-nowTime);
        return Note.HitStatue.None.setOffset( hitTime-nowTime);
    }

    private void genEffect(
            LatosStage effectStage,float[] foot, HitEffect.Type type){
        HitEffect actor =
                new HitEffect(type,
                        (int)foot[0],(int)foot[1]);
        effectStage.addElement(actor);
    }

    private void genBadEffect(LatosStage effectStage,Note badNote){
        effectStage.addElement(new BadEffect(badNote));
    }

    private void playSound(Note note){
        Note.NoteType noteType = note.getNoteType();
        Sound tapSound = Res.getInstance().get("tapSound", Sound.class);
        Sound dragSound = Res.getInstance().get("dragSound", Sound.class);
        Sound flickSound = Res.getInstance().get("flickSound", Sound.class);
        switch (noteType) {
            case Tap:
            case Hold:
                tapSound.play();
                break;
            case Drag:
                dragSound.play();
                break;
            case Flick:
            case DirectFlick:
                flickSound.play();
                break;
            default:
        }
    }

    public static class JudgeResult{
        public boolean hit=false;
        public boolean delete=true;
        public boolean missed=false;
        public boolean updateJudgeIndex=true;
        public Note.HitStatue hitStatue= Note.HitStatue.None;
    }
}
